/*
 * Copyright 2003,2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package eu.jakubiak.jpa.translator;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import eu.jakubiak.jpa.translator.annotations.Translatable;
import eu.jakubiak.jpa.translator.annotations.Translate;
import eu.jakubiak.jpa.translator.annotations.TranslatorFollowMany;
import eu.jakubiak.jpa.translator.annotations.TranslatorFollowOne;
import eu.jakubiak.jpa.translator.discriminator.Discriminator;
import eu.jakubiak.jpa.translator.provider.DefaultLocaleProvider;
import eu.jakubiak.jpa.translator.provider.LocaleProvider;
import eu.jakubiak.jpa.translator.provider.Provider;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyCollection;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyList;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxyMap;
import eu.jakubiak.jpa.translator.proxy.collections.TranslatorProxySet;
import eu.jakubiak.jpa.translator.serializer.Serializer;
import eu.jakubiak.javabean.BeanProperty;
import eu.jakubiak.javabean.BeanReflector;

/**
 * Translator class
 * 
 * 
 * 
 * @author jakubiak_a
 */
public class Translator {
	/**
	 * Current locale for tranlated objects
	 */
	private LocaleProvider localeProvider;
	/**
	 * Provider instance this override
	 * 
	 * @Translate(provider)
	 */
	private Provider provider;
	/**
	 * Discriminator instance this override
	 * 
	 * @Translate(discriminator)
	 */
	private Discriminator discriminator;
	/**
	 * Serializer instance this override
	 * 
	 * @Translate(serializer)
	 */
	private Serializer serializer;

	/**
	 * Mark allready translated object This interface is used to create proxy
	 * class
	 */
	public static interface AlreadyTranslated {

		/**
		 * get delegate for proxy object this method shoud not be called
		 * directly
		 * 
		 * @param dummy
		 *            unused parameter which helps method inteceptor to identify
		 *            this method
		 * @return
		 */
		Object getDelegate(AlreadyTranslated dummy);

		/**
		 * get translator for proxy object this method shoud not be called
		 * directly
		 * 
		 * @param dummy
		 *            unused parameter which helps method inteceptor to identify
		 *            this method
		 * @return
		 */
		Translator getTranslator(AlreadyTranslated dummy);
	}

	private class TranslatorProxyMethodInterceptor implements MethodInterceptor {

		private Translator translator;
		private Object delegate;
		private BeanReflector reflector;

		public TranslatorProxyMethodInterceptor(Class clazz, Object delegate,
				Translator translator) {
			this.delegate = delegate;
			this.reflector = new BeanReflector(clazz);
			this.translator = translator;
		}

		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy proxy) throws Throwable {

			if (Translator.AlreadyTranslated.class.getMethod("getDelegate",
					Translator.AlreadyTranslated.class).equals(method)) {
				return this.delegate;
			}
			if (Translator.AlreadyTranslated.class.getMethod("getTranslator",
					Translator.AlreadyTranslated.class).equals(method)) {
				return this.translator;
			}

			if (method.getName().startsWith("set")) {
				if (reflector.containsMethod(method.getName())) {
					BeanProperty property = reflector.getProperty(lowerCaseFirst(method.getName().substring(3)));
					if (property.isAnnotationPresent(Translate.class)) {
						return set(property, args[0]);
					}
				}
			}
			if (method.getName().startsWith("get")) {
				if (reflector.containsMethod(method.getName())) {
					BeanProperty property = reflector.getProperty(lowerCaseFirst(method.getName().substring(3)));
					if (property.isAnnotationPresent(Translate.class)) {
						return get(property);
					}
					if (property.isAnnotationPresent(TranslatorFollowOne.class)) {
						TranslatorFollowOne tfo = property
								.getAnnotation(TranslatorFollowOne.class);
						Class targetClass = tfo.targetClass();
						if (void.class.equals(targetClass)) {
							targetClass = property.getType();
						}
						return translator.translate(targetClass, get(property));
					}
					if (property
							.isAnnotationPresent(TranslatorFollowMany.class)) {
						if (!property.getType().isInterface()) {
							throw new RuntimeException(
									"TranslatorFallowMany require interface: "
											+ property.getType());
						} else {
							Object returnValue = get(property);
							TranslatorFollowMany tfm = property
									.getAnnotation(TranslatorFollowMany.class);
							Class targetClass = tfm.targetClass();
							if (void.class.equals(targetClass)) {
								Type type = property.getParameterizedType()
										.getActualTypeArguments()[0];
								if (type == null) {
									throw new RuntimeException(
											"Cannot read a collection type");
								}
								targetClass = (Class) type;
							}
							if (returnValue != null) {
								if (returnValue instanceof Map) {
									return new TranslatorProxyMap(targetClass,
											translator, (Map) returnValue);
								} else if (returnValue instanceof Set) {
									return new TranslatorProxySet(targetClass,
											translator, (Set) returnValue);
								} else if (returnValue instanceof List) {
									return new TranslatorProxyList(targetClass,
											translator, (List) returnValue);
								} else if (returnValue instanceof Collection) {
									return new TranslatorProxyCollection(
											targetClass, translator,
											(Collection) returnValue);
								} else {
									throw new RuntimeException(
											"Unsupported collection type: "
													+ returnValue.getClass()
															.getName());
								}
							}
						}
					}
				}
			}
			if (method.getName().startsWith("is")) {
				if (reflector.containsMethod(method.getName())) {
					BeanProperty property = reflector.getProperty(lowerCaseFirst(method.getName().substring(2)));
					if (property.isAnnotationPresent(Translate.class)) {
						return get(property);
					}
				}
			}
			return method.invoke(delegate, args);
		}

		private Object get(BeanProperty property) throws Throwable {
			return translator.read(property, delegate);
		}

		private Object set(BeanProperty property, Object value)
				throws Throwable {
			translator.store(property, delegate, value);
			return null;
		}
	}
	
    private String lowerCaseFirst(String string) {
        return String.valueOf(string.charAt(0)).toLowerCase()+string.substring(1);
    }
	

	/**
	 * Create new translator
	 * 
	 * @param locale
	 */
	public Translator(Locale locale) {
		localeProvider=new DefaultLocaleProvider(locale);
		if (locale == null) {
			throw new RuntimeException("Error while initializing translator, please provide a non null locale");
		}
	}
	/**
	 * Create new translator
	 * 
	 * @param locale
	 */
	public Translator(LocaleProvider localeProvider) {
		this.localeProvider=localeProvider;
		if (localeProvider.getLocale() == null) {
			throw new RuntimeException("Error while initializing translator, LocaleProvider returned null!");
		}
	}

	/**
	 * Get current locale
	 * 
	 * @return current locale used in translator
	 */
	public Locale getLocale() {
		return localeProvider.getLocale();
	}

	public <T> T translate(T object) {
		Class<T> clazz = (Class<T>) object.getClass();
		return translate(clazz, object);
	}

	/**
	 * Create translated proxy object
	 * 
	 * @param object
	 *            for translation
	 * @return proxy object
	 */
	public <T> T translate(Class<T> clazz, T object) {

		if (object == null) {
			// I don't know how to read null
			return null;
		}

		if (isTranslated(object)) {
			// I want to translate proxy only for objects which wasn't allready
			// translated
			// this object was allready translated
			return object;
		}

		if (!clazz.isAnnotationPresent(Translatable.class)) {
			// This object is not translatable
			return object;
		}

		MethodInterceptor methodInterceptor = new TranslatorProxyMethodInterceptor(
				clazz, object, this);
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(clazz);
		enhancer.setInterfaces(new Class[] { AlreadyTranslated.class });
		enhancer.setCallback(methodInterceptor);
		return (T) enhancer.create();
	}

	/**
	 * Check, if an object is translated
	 * 
	 * @param object
	 *            to test
	 * @return true if object is translated
	 */
	public static boolean isTranslated(Object object) {
		return object instanceof AlreadyTranslated;
	}

	/**
	 * Get delegate object for translated proxy object
	 * 
	 * @param translatedObject
	 * @return
	 */
	public static Object getDelegate(Object translatedObject) {
		AlreadyTranslated proxy = (AlreadyTranslated) translatedObject;
		return proxy.getDelegate(proxy);
	}

	/**
	 * Get translator used to translate object
	 * 
	 * @param translatedObject
	 * @return
	 */
	public static Translator getTranslator(Object translatedObject) {
		AlreadyTranslated proxy = (AlreadyTranslated) translatedObject;
		return proxy.getTranslator(proxy);
	}

	public Discriminator getDiscriminator() {
		return discriminator;
	}

	public void setDiscriminator(Discriminator discriminator) {
		this.discriminator = discriminator;
	}

	public Provider getProvider() {
		return provider;
	}

	public void setProvider(Provider provider) {
		this.provider = provider;
	}

	public Serializer getSerializer() {
		return serializer;
	}

	public void setSerializer(Serializer serializer) {
		this.serializer = serializer;
	}

	/**
	 * Read translation from provider
	 * 
	 * @param property
	 * @param object
	 * @return
	 * @throws java.lang.IllegalAccessException
	 * @throws java.lang.IllegalArgumentException
	 * @throws java.lang.reflect.InvocationTargetException
	 * @throws java.lang.InstantiationException
	 */
	private Object read(BeanProperty property, Object object)
			throws IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, InstantiationException {

		if (!property.isAnnotationPresent(Translate.class)) {
			return property.get(object);
		}

		Translate translatable = (Translate) property
				.getAnnotation(Translate.class);

		TranslationKey key = createKey(property, object);

		String translatedText = getProviderFor(translatable).get(key);

		Object returnValue = getSerializerFor(translatable).deserialize(
				property.getType(), translatedText);
		if (returnValue != null) {
			return returnValue;
		}

		return property.get(object);
	}

	/**
	 * Persist translation in provider
	 * 
	 * @param property
	 * @param object
	 * @param translatedValue
	 * @throws java.lang.IllegalAccessException
	 * @throws java.lang.IllegalArgumentException
	 * @throws java.lang.reflect.InvocationTargetException
	 * @throws java.lang.InstantiationException
	 */
	private void store(BeanProperty property, Object object,
			Object translatedValue) throws IllegalAccessException,
			IllegalArgumentException, InvocationTargetException,
			InstantiationException {

		if (!property.isAnnotationPresent(Translate.class)) {
			property.set(object, translatedValue);
			return;
		}

		Translate translatable = (Translate) property
				.getAnnotation(Translate.class);
		String translatedString = getSerializerFor(translatable).serialize(
				property.getType(), translatedValue);

		TranslationKey key = createKey(property, object);
		getProviderFor(translatable).set(key, translatedString);
	}

	/**
	 * Get provider XXX kod tej metody powinień być przeniesiony do osobnej
	 * klasy
	 * 
	 * @param translate
	 * @return
	 * @throws java.lang.InstantiationException
	 * @throws java.lang.IllegalAccessException
	 */
	private Provider getProviderFor(Translate translate)
			throws InstantiationException, IllegalAccessException {
		if (getProvider() != null) {
			return getProvider();
		}
		if (!void.class.equals(translate.provider())) {
			return translate.provider().newInstance();
		}
		return TranslatorProperties.getProviderClass().newInstance();
	}

	/**
	 * Get serializer XXX kod tej metody powinień być przeniesiony do osobnej
	 * klasy
	 * 
	 * @param translate
	 * @return
	 * @throws java.lang.InstantiationException
	 * @throws java.lang.IllegalAccessException
	 */
	private Serializer getSerializerFor(Translate translate)
			throws InstantiationException, IllegalAccessException {
		if (getSerializer() != null) {
			return getSerializer();
		}
		if (!void.class.equals(translate.serializer())) {
			return translate.serializer().newInstance();
		}
		return TranslatorProperties.getSerializerClass().newInstance();
	}

	/**
	 * Get discriminator XXX kod tej metody powinień być przeniesiony do osobnej
	 * klasy
	 * 
	 * @param translate
	 * @return
	 * @throws java.lang.InstantiationException
	 * @throws java.lang.IllegalAccessException
	 */
	private Discriminator getDiscriminatorFor(Translate translate)
			throws InstantiationException, IllegalAccessException {
		if (getDiscriminator() != null) {
			return getDiscriminator();
		}
		if (!void.class.equals(translate.discriminator())) {
			return translate.discriminator().newInstance();
		}
		return TranslatorProperties.getDiscriminatorClass().newInstance();
	}

	/**
	 * Creating key for find it in translation table
	 * 
	 * @param property
	 * @param object
	 * @return
	 * @throws java.lang.InstantiationException
	 * @throws java.lang.IllegalAccessException
	 * @throws java.lang.IllegalArgumentException
	 * @throws java.lang.reflect.InvocationTargetException
	 */
	private TranslationKey createKey(BeanProperty property, Object object)
			throws InstantiationException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException {
		Translate translatable = (Translate) property
				.getAnnotation(Translate.class);
		return new TranslationKey(property, object, getLocale(),
				getSerializerFor(translatable),
				getDiscriminatorFor(translatable));
	}

	@Override
	public boolean equals(Object obj) {
		return this == obj;
	}

	@Override
	public int hashCode() {
		int hash = 7;
		return hash;
	}

}
